using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using ICSharpCode.SharpZipLib.Checksums;
using System.Collections;

namespace GwtRpcNet.Rpc
{
    public class RpcNetWriter
    {
        #region const Members

        public const int SERIALIZATION_STREAM_FLAGS_NO_TYPE_VERSIONING = 1;
        public const int SERIALIZATION_STREAM_VERSION_WITHOUT_SERIALIZATION_POLICY = 2;
        public const int SERIALIZATION_STREAM_VERSION = 3;

        private const char NON_BREAKING_HYPHEN = '\u2011';
        private const int NUMBER_OF_JS_ESCAPED_CHARS = 128;
        private static char[] JS_CHARS_ESCAPED;
        private const char JS_ESCAPE_CHAR = '\\';
        private const char JS_QUOTE_CHAR = '\"';
        private static char[] NIBBLE_TO_HEX_CHAR = new char[]{
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

        static RpcNetWriter()
        {
            JS_CHARS_ESCAPED = new char[NUMBER_OF_JS_ESCAPED_CHARS];
            JS_CHARS_ESCAPED['\u0000'] = '0';
            JS_CHARS_ESCAPED['\b'] = 'b';
            JS_CHARS_ESCAPED['\t'] = 't';
            JS_CHARS_ESCAPED['\n'] = 'n';
            JS_CHARS_ESCAPED['\f'] = 'f';
            JS_CHARS_ESCAPED['\r'] = 'r';
            JS_CHARS_ESCAPED[JS_ESCAPE_CHAR] = JS_ESCAPE_CHAR;
            JS_CHARS_ESCAPED[JS_QUOTE_CHAR] = JS_QUOTE_CHAR;
        }

        #endregion

        private MethodInfo method;
        private RpcNetRequest request;
        private object result;

        private int objectCount = 0;
        private int tokenListCharCount = 0;
        private Dictionary<object, int> objectMap = new Dictionary<object, int>();
        private Dictionary<string, int> stringMap = new Dictionary<string, int>();
        private List<string> stringTable = new List<string>();
        private List<string> tokenList = new List<string>();
        private static Hashtable classCRC32Cache = Hashtable.Synchronized(new Hashtable());

        public string EncodeResponse(MethodInfo method, RpcNetRequest request, object result, bool wasThrown)
        {
            this.method = method;
            this.request = request;
            this.result = result;

            if (CheckTypes(method, result) && method.ReturnType != typeof(void))
                SerializeValue(result, method.ReturnType);

            return (wasThrown ? "//EX" : "//OK") + ToString();
        }

        private void SerializeValue(object result, Type valueType)
        {
            if (valueType == typeof(bool))
                WriteBool(Convert.ToBoolean(result));
            else if (valueType == typeof(byte))
                WriteByte(Convert.ToByte(result));
            else if (valueType == typeof(char))
                WriteChar(Convert.ToChar(result));
            else if (valueType == typeof(double))
                WriteDouble(Convert.ToDouble(result));
            else if (valueType == typeof(float))
                WriteFloat(Convert.ToDouble(result));
            else if (valueType == typeof(int))
                WriteInt(Convert.ToInt32(result));
            else if (valueType == typeof(long))
                WriteLong(Convert.ToInt64(result));
            else if (valueType == typeof(short))
                WriteShort(Convert.ToInt16(result));
            else if (valueType == typeof(string))
                WriteString(Convert.ToString(result));
            else 
                WriteObject(result);
        }

        private void Serialize(object instance, string typeSignature)
        {
            Type instanceClass = instance.GetType();
            Type customSerializer = RpcNet.HasCustomFieldSerializer(instanceClass);
            if (customSerializer != null)
                SerializeWithCustomSerializer(customSerializer, instance, instanceClass);
            else
                SerializeClass(instance, instanceClass);
        }

        private void SerializeWithCustomSerializer(Type customSerializer, object instance, Type instanceClass)
        {
            MethodInfo serialize;
            try
            {
                if (instanceClass.IsArray)
                    throw new NotImplementedException();
                serialize = customSerializer.GetMethod("Serialize", new Type[] {
                    typeof(RpcNetWriter), instanceClass});
                serialize.Invoke(null, new object[] { this, instance });
            }
            catch (System.Exception e)
            {
                throw e;
            }
        }

        private void SerializeClass(object instance, Type instanceClass)
        {
            PropertyInfo[] props = instanceClass.GetProperties();
            Array.Sort(props, new PropertyInfoComparer());
            foreach (PropertyInfo prop in props)
            {
                object value = prop.GetValue(instance, null);
                SerializeValue(value, prop.PropertyType);
            }

            Type baseType = instanceClass.BaseType;

            /**TODO check if base type needs to be serialized**/
            //Class superClass = instanceClass.getSuperclass();
            //if (serializationPolicy.shouldSerializeFields(superClass))
            //{
            //    serializeImpl(instance, superClass);
            //}
        }

        private void Append(string token)
        {
            tokenList.Add(token);
            if (token != null)
                tokenListCharCount += token.Length;
        }

        private int AddString(string s)
        {
            if (s == null)
                return 0;
            if (stringMap.ContainsKey(s))
                return stringMap[s];
            stringTable.Add(s);
            // index is 1-based
            int index = stringTable.Count;
            stringMap.Add(s, index);
            return index;
        }

        public override string ToString()
        {
            int capacityGuess = 2 * tokenListCharCount + 2 * tokenList.Count;
            StringBuilder buffer = new StringBuilder(capacityGuess);
            buffer.Append("[");
            WritePayload(buffer);
            WriteStringTable(buffer);
            WriteHeader(buffer);
            buffer.Append("]");
            return buffer.ToString();
        }

        #region Write

        public void WriteBool(bool p)
        {
            Append(p ? "1" : "0");
        }

        public void WriteByte(byte p)
        {
            Append(Convert.ToString(p));
        }

        public void WriteChar(char p)
        {
            Append(Convert.ToString((int)p));
        }

        public void WriteDouble(double p)
        {
            Append(Convert.ToString(p));
        }

        public void WriteFloat(double p)
        {
            Append(Convert.ToString(p));
        }

        public void WriteInt(int p)
        {
            Append(Convert.ToString(p));
        }

        public void WriteLong(long p)
        {
            Append(Convert.ToString(p));
        }

        public void WriteShort(short p)
        {
            Append(Convert.ToString(p));
        }

        public void WriteString(string p)
        {
            WriteInt(AddString(p));
        }

        internal void WriteObject(object instance)
        {
            if (RpcNet.IsPrimitiveType(instance.GetType()))
            {
                SerializeValue(instance, instance.GetType());
                return;
            }

            if (instance == null)
            {
                WriteString(null);
                return;
            }

            int objIndex = GetIndexForObject(instance);
            if (objIndex >= 0)
            {
                // We've already encoded this object, make a backref
                // Transform 0-based to negative 1-based
                WriteInt(-(objIndex + 1));
                return;
            }

            SaveIndexForObject(instance);

            // Serialize the type signature
            string typeSignature = GetObjectTypeSignature(instance);
            WriteString(typeSignature);
            // Now serialize the rest of the object
            Serialize(instance, typeSignature);
        }

        private void WriteHeader(StringBuilder buffer)
        {
            buffer.Append(",");
            buffer.Append(request.Flags);
            buffer.Append(",");
            buffer.Append(request.Version);
        }

        private void WritePayload(StringBuilder buffer)
        {
            for (int i = tokenList.Count - 1; i >= 0; --i)
            {
                string token = tokenList[i];
                buffer.Append(token);
                if (i > 0)
                    buffer.Append(",");
            }
        }

        private void WriteStringTable(StringBuilder buffer)
        {
            if (tokenList.Count > 0)
                buffer.Append(",");
            buffer.Append("[");
            for (int i = 0, c = stringTable.Count; i < c; ++i)
            {
                if (i > 0)
                    buffer.Append(",");
                buffer.Append(EscapeString(stringTable[i]));
            }
            buffer.Append("]");
        }

        #endregion

        #region Check

        private bool CheckTypes(MethodInfo method, object result) 
        {
            Type methodReturnType = method.ReturnType;
            if (methodReturnType != typeof(void) && result != null) 
            {
                Type actualReturnType = result.GetType();

                if (actualReturnType == null
                    || !methodReturnType.IsAssignableFrom(actualReturnType)) {
                    throw new System.Exception("Type '"
                        + result.GetType().FullName
                        + "' does not match the return type in the method's signature: '"
                        + method.Name + "'");
                }
            }
            return true;
        }

        #endregion

        #region Helper Methods

        /**
        * This method takes a string and outputs a JavaScript string literal. The
        * data is surrounded with quotes, and any contained characters that need to
        * be escaped are mapped onto their escape sequence.
        * 
        * Assumptions: We are targeting a version of JavaScript that that is later
        * than 1.3 that supports unicode strings.
        */
        private static string EscapeString(string toEscape)
        {
            // make output big enough to escape every character (plus the quotes)
            char[] input = toEscape.ToCharArray();
            List<char> charVector = new List<char>();
            charVector.Add(JS_QUOTE_CHAR);

            for (int i = 0, n = input.Length; i < n; ++i)
            {
                char c = input[i];
                if (c < NUMBER_OF_JS_ESCAPED_CHARS && JS_CHARS_ESCAPED[c] != 0)
                {
                    charVector.Add(JS_ESCAPE_CHAR);
                    charVector.Add(JS_CHARS_ESCAPED[c]);
                }
                else if (NeedsUnicodeEscape(c))
                {
                    charVector.Add(JS_ESCAPE_CHAR);
                    UnicodeEscape(c, charVector);
                }
                else
                {
                    charVector.Add(c);
                }
            }

            charVector.Add(JS_QUOTE_CHAR);
            return new string(charVector.ToArray());
        }

        private static bool NeedsUnicodeEscape(char ch)
        {
            switch (java.lang.Character.getType(ch))
            {
                // Conservative
                case java.lang.Character.COMBINING_SPACING_MARK:
                case java.lang.Character.ENCLOSING_MARK:
                case java.lang.Character.NON_SPACING_MARK:
                case java.lang.Character.UNASSIGNED:
                case java.lang.Character.PRIVATE_USE:
                case java.lang.Character.SPACE_SEPARATOR:
                case java.lang.Character.CONTROL:

                // Minimal
                case java.lang.Character.LINE_SEPARATOR:
                case java.lang.Character.FORMAT:
                case java.lang.Character.PARAGRAPH_SEPARATOR:
                case java.lang.Character.SURROGATE:
                    return true;

                default:
                    if (ch == NON_BREAKING_HYPHEN)
                    {
                        // This can be expanded into a break followed by a hyphen
                        return true;
                    }
                    break;
            }

            return false;
        }

        private static void UnicodeEscape(char ch, List<char> charVector)
        {
            if (ch < 256)
            {
                charVector.Add('x');
                charVector.Add(NIBBLE_TO_HEX_CHAR[(ch >> 4) & 0x0F]);
                charVector.Add(NIBBLE_TO_HEX_CHAR[ch & 0x0F]);
            }
            else
            {
                charVector.Add('u');
                charVector.Add(NIBBLE_TO_HEX_CHAR[(ch >> 12) & 0x0F]);
                charVector.Add(NIBBLE_TO_HEX_CHAR[(ch >> 8) & 0x0F]);
                charVector.Add(NIBBLE_TO_HEX_CHAR[(ch >> 4) & 0x0F]);
                charVector.Add(NIBBLE_TO_HEX_CHAR[ch & 0x0F]);
            }
        }

        protected int GetIndexForObject(object instance)
        {
            if (objectMap.ContainsKey(instance))
                return objectMap[instance];
            return -1;
        }

        protected string GetObjectTypeSignature(object instance)
        {
            if (ShouldEnforceTypeVersioning())
                return EncodeSerializedInstanceReference(instance.GetType());
            else
                return GetSerializedTypeName(instance.GetType());
        }

        protected void SaveIndexForObject(object instance)
        {
            objectMap.Add(instance, objectCount++);
        }

        public bool ShouldEnforceTypeVersioning()
        {
            return (request.Flags & SERIALIZATION_STREAM_FLAGS_NO_TYPE_VERSIONING) == 0;
        }

        public static string GetSerializedTypeName(Type t)
        {
            if (RpcNet.TYPE_NAMES_SERIALIZED.ContainsKey(t))
                return RpcNet.TYPE_NAMES_SERIALIZED[t];
            return t.FullName;
        }

        public static string EncodeSerializedInstanceReference(Type instanceType)
        {
            string typeName = GetSerializedTypeName(instanceType);
            return typeName
                + RpcNet.SERIALIZED_REFERENCE_SEPARATOR
                + GetSerializationSignature(instanceType);
        }

        public static string GetSerializationSignature(Type instanceType)
        {
            if (!classCRC32Cache.ContainsKey(instanceType))
            {
                Crc32 crc = new Crc32();
                try
                {
                    GenerateSerializationSignature(instanceType, crc);
                }
                catch (System.Exception e)
                {
                    throw new Exception(
                        "Could not compute the serialization signature", e);
                }
                string result = Convert.ToString(crc.Value);
                classCRC32Cache.Add(instanceType, result);
            }
            return (string)classCRC32Cache[instanceType];
        }

        private static void GenerateSerializationSignature(Type instanceType, Crc32 crc)
        {
            string typeName = GetSerializedTypeName(instanceType);
            crc.Update(Encoding.UTF8.GetBytes(typeName));

            if (RpcNet.TYPES_WHOSE_IMPLEMENTATION_IS_EXCLUDED_FROM_SIGNATURES.ContainsKey(instanceType))
                return;

            Type customSerializer = RpcNet.HasCustomFieldSerializer(instanceType);
            if (customSerializer != null)
                GenerateSerializationSignature(customSerializer, crc);
            else if (instanceType.IsArray)
                GenerateSerializationSignature(instanceType.GetElementType(), crc);
            else if (!instanceType.IsPrimitive)
            {
                PropertyInfo[] props = instanceType.GetProperties();
                Array.Sort(props, new PropertyInfoComparer());
                foreach (PropertyInfo prop in props)
                {
                    crc.Update(Encoding.UTF8.GetBytes(prop.Name));
                    crc.Update(Encoding.UTF8.GetBytes(GetSerializedTypeName(prop.PropertyType)));
                }
                Type baseType = instanceType.BaseType;
                if (baseType != null)
                    GenerateSerializationSignature(baseType, crc);
            }
        }

        #endregion
    }
}
